from __future__ import annotations

import os
import sys

import pytest

from dynaconf import Dynaconf
from dynaconf import LazySettings
from dynaconf import ValidationError
from dynaconf import Validator
from dynaconf.loaders import toml_loader
from dynaconf.loaders import yaml_loader
from dynaconf.nodes import DataDict
from dynaconf.nodes import DataList
from dynaconf.strategies.filtering import PrefixFilter
from dynaconf.utils.parse_conf import true_values


def test_deleted_raise(settings):
    """asserts variable can be deleted"""
    assert "TODELETE" in settings
    assert settings.TODELETE is True
    del settings.TODELETE
    with pytest.raises(AttributeError):
        assert settings.TODELETE is True
    assert settings.exists("TODELETE") is False
    assert settings.get("TODELETE") is None

    # case: del using lowercase when original was mixed-case
    settings.ToDelete3 = True
    assert "ToDelete3" in settings
    assert settings.TODELETE3 is True
    del settings.todelete3
    with pytest.raises(AttributeError):
        assert settings.TODELETE3 is True
    assert settings.exists("TODELETE3") is False
    assert settings.get("TODELETE3") is None

    # case: del nested attribute
    settings.set("Abc.Xyz.Uv", 123)
    assert settings.abc.xyz.uv == 123
    del settings.abc.xyz.uv
    with pytest.raises(AttributeError):
        assert settings.abc.xyz.uv == 123
    assert settings.exists("abc.xyz") is True
    assert settings.exists("abc.xyz.uv") is False
    assert settings.get("abc.xyz.uv") is None

    # case: del item
    settings.TODELETE4 = True
    assert "TODELETE4" in settings
    assert settings.TODELETE4 is True
    del settings["TODELETE4"]
    with pytest.raises(AttributeError):
        assert settings.TODELETE4 is True
    assert settings.exists("TODELETE4") is False
    assert settings.get("TODELETE4") is None

    # case: del item lowercase when orifinal was mixed-case
    settings.ToDelete3 = True
    assert "ToDelete3" in settings
    assert settings.TODELETE3 is True
    del settings["todelete3"]
    with pytest.raises(AttributeError):
        assert settings.TODELETE3 is True
    assert settings.exists("TODELETE3") is False
    assert settings.get("TODELETE3") is None


def test_delete_and_set_again(settings):
    """asserts variable can be deleted and set again"""

    # set
    settings.TODELETE2 = True
    assert "TODELETE2" in settings
    assert settings.TODELETE2 is True

    # delete
    del settings.TODELETE2
    assert settings.exists("TODELETE2") is False
    assert settings.get("TODELETE2") is None

    # set again
    settings.TODELETE2 = "new value"
    assert settings.TODELETE2 == "new value"


def test_accepts_only_upper():
    """Only upper case names are allowed if lowercase_read=False
    lower case are converted"""

    settings = LazySettings(debug=True, lowercase_read=False)

    assert settings.DEBUG is True
    assert settings.get("debug") is True
    assert settings.get("DEBUG") is True
    assert settings.get("Debug") is True
    assert settings.exists("debug")
    with pytest.raises(AttributeError):
        # access in lower case is not allowed
        assert settings.debug is True


def test_populate_obj(settings):
    class Obj:
        pass

    obj = Obj()

    settings.populate_obj(obj)

    assert obj.DEBUG is True
    assert obj.VALUE == 42.1


def test_populate_obj_with_keys(settings):
    class Obj:
        pass

    obj = Obj()

    settings.populate_obj(obj, ["VALUE"])

    assert obj.VALUE == 42.1

    with pytest.raises(AttributeError):
        assert obj.DEBUG is True


def test_populate_obj_with_ignore(settings):
    class Obj:
        pass

    obj = Obj()

    settings.populate_obj(obj, ignore=["VALUE"])

    assert obj.DEBUG is True

    with pytest.raises(AttributeError):
        assert obj.VALUE == 42.1


def test_populate_obj_convert_to_dict(settings):
    class Obj:
        pass

    # first make sure regular populate brings in DataDict and DataList
    obj = Obj()
    settings.populate_obj(obj)
    assert isinstance(obj.ADICT, DataDict)
    assert isinstance(obj.ALIST, DataList)
    assert isinstance(obj.ADICT.to_yaml(), str)

    # now make sure convert_to_dict=True brings in dict and list
    obj = Obj()
    settings.populate_obj(obj, convert_to_dict=True)
    assert isinstance(obj.ADICT, dict)
    assert isinstance(obj.ALIST, list)
    with pytest.raises(AttributeError):
        assert isinstance(obj.ADICT.to_yaml(), str)


def test_call_works_as_get(settings):
    """settings.get('name') is the same as settings('name')"""

    assert settings("debug") == settings.get("DEBUG")
    assert settings("non_exist", default="hello") == "hello"
    assert settings.get("non_exist", default="hello") == "hello"
    assert settings.__call__("debug") == settings.DEBUG
    assert settings._wrapped.__call__("debug") == settings.DEBUG


def test_keys_are_equal(settings):
    assert set(list(settings.keys())) == set(list(settings.store.keys()))


def test_values_are_equal(settings):
    for item in settings.values():
        assert item in settings.store.values()


def test_get_env(settings):
    settings.environ["FRUIT"] = "BANANA"
    assert settings.exists_in_environ("fruit") is True
    assert settings.environ["FRUIT"] == settings.get_environ("fruit")
    assert os.environ["FRUIT"] == settings.environ.get("FRUIT")

    settings.environ["SALARY"] = "180.235"
    assert settings.get_environ("salary", cast="@float") == 180.235

    settings.environ["ENVINT"] = "@int 24"
    assert settings.get_environ("envint", cast=True) == 24


def test_float(settings):
    settings.set("money", "500.42")
    config = settings.__core__.config
    assert settings.exists("MONEY")
    assert settings.MONEY == "500.42"
    assert settings.MONEY != 500.42
    assert settings.store["MONEY"] == "500.42"
    assert "MONEY" not in config.deleted
    assert "money" not in config.deleted
    assert isinstance(settings.as_float("money"), float)
    assert settings.as_float("MONEY") == 500.42


def test_int(settings):
    settings.set("age", "500")
    config = settings.__core__.config
    assert settings.exists("AGE")
    assert settings.AGE == "500"
    assert settings.AGE != 500
    assert settings.store["AGE"] == "500"
    assert "AGE" not in config.deleted
    assert "age" not in config.deleted
    assert isinstance(settings.as_int("age"), int)
    assert settings.as_int("age") == 500


def test_bool(settings):
    config = settings.__core__.config
    for true_value in true_values:
        # ('t', 'true', 'enabled', '1', 'on', 'yes')
        settings.set("feature", true_value)
        config = settings.__core__.config
        assert settings.exists("FEATURE")
        assert settings.FEATURE == true_value
        assert settings.FEATURE is not True
        assert settings.store["FEATURE"] == true_value
        assert "FEATURE" not in config.deleted
        assert "feature" not in config.deleted
        assert isinstance(settings.as_bool("feature"), bool)
        assert settings.as_bool("FEATURE") is True

    # anything else is a false value
    false_values = ["f", "false", "False", "disabled", "0", "off", "no"]
    for false_value in false_values:
        settings.set("feature", false_value)
        assert settings.exists("FEATURE")
        assert settings.FEATURE == false_value
        assert settings.FEATURE is not False
        assert settings.store["FEATURE"] == false_value
        assert "FEATURE" not in config.deleted
        assert "feature" not in config.deleted
        assert isinstance(settings.as_bool("feature"), bool)
        assert settings.as_bool("FEATURE") is False


def test_as_json(settings):
    settings.set("fruits", '["banana", "apple", "kiwi"]')
    config = settings.__core__.config
    assert settings.exists("FRUITS")
    assert settings.FRUITS == '["banana", "apple", "kiwi"]'
    assert settings.FRUITS != ["banana", "apple", "kiwi"]
    assert settings.store["FRUITS"] == '["banana", "apple", "kiwi"]'
    assert "FRUITS" not in config.deleted
    assert "fruits" not in config.deleted
    assert isinstance(settings.as_json("fruits"), list)
    assert settings.as_json("fruits") == ["banana", "apple", "kiwi"]

    settings.set("person", '{"name": "Bruno"}')
    assert settings.exists("PERSON")
    assert settings.PERSON == '{"name": "Bruno"}'
    assert settings.PERSON != {"name": "Bruno"}
    assert settings.store["PERSON"] == '{"name": "Bruno"}'
    assert "PERSON" not in config.deleted
    assert "person" not in config.deleted
    assert isinstance(settings.as_json("person"), dict)
    assert settings.as_json("person") == {"name": "Bruno"}


def test_env_should_be_string(settings):
    with pytest.raises(ValueError):
        settings.setenv(123456)


def test_env_should_allow_underline(settings):
    settings.setenv("COOL_env")
    assert settings.current_env == "COOL_ENV"


def test_path_for(settings):
    assert settings.path_for(os.path.sep, "tmp", "bla") == os.path.join(
        os.path.sep, "tmp", "bla"
    )
    assert settings.path_for("foo", "bar", "blaz") == os.path.join(
        settings._root_path, "foo", "bar", "blaz"
    )


def test_get_item(settings):
    assert settings["DOTENV_INT"] == 1
    assert settings["PORT"] == 5000
    with pytest.raises(KeyError):
        settings["DONOTEXISTTHISKEY"]


def test_set_item(settings):
    settings["FOO"] = "bar"
    assert settings.FOO == "bar"
    # assert "FOO" in settings._defaults
    assert settings("FOO") == "bar"
    assert settings.get("FOO") == "bar"


def test_set(settings):
    # NOTE: it is recommended to call set(x, 1) or ['x'] = 1
    # instead of settings.BAZ = 'bar'
    settings.set("BAZ", "bar")
    assert settings.BAZ == "bar"
    # assert "BAZ" in settings._defaults
    assert settings("BAZ") == "bar"
    assert settings.get("BAZ") == "bar"


def test_global_set_merge(settings):
    settings.set("MERGE_ENABLED_FOR_DYNACONF", True)
    settings.set(
        "MERGE_KEY", {"items": [{"name": "item 1"}, {"name": "item 2"}]}
    )
    settings.set(
        "MERGE_KEY", {"items": [{"name": "item 3"}, {"name": "item 4"}]}
    )
    assert settings.MERGE_KEY == {
        "items": [
            {"name": "item 1"},
            {"name": "item 2"},
            {"name": "item 3"},
            {"name": "item 4"},
        ]
    }


def test_global_set_merge_false():
    # data
    list_old = [1, 2, 3]
    list_new = [4, 5, 6]
    list_expected = [4, 5, 6]
    dict_old = {"a": {"b": "B"}}
    dict_new = {"a": {"c": "C"}}
    dict_expected = {"a": {"c": "C"}}

    # environment = False
    settings_a = Dynaconf(
        environments=False,
        merge_enabled=False,
    )
    settings_a.set("default", {"dicty": dict_old, "listy": list_old})
    settings_a.set("default", {"dicty": dict_new, "listy": list_new})

    assert settings_a.default.dicty == dict_expected
    assert settings_a.default.listy == list_expected

    # environment = True
    settings_b = Dynaconf(
        environments=True,
        merge_enabled=False,
    )
    settings_b.set("test", {"dicty": dict_old, "listy": list_old})
    settings_b.set("test", {"dicty": dict_new, "listy": list_new})
    assert settings_b.test.dicty == dict_expected
    assert settings_b.test.listy == list_expected


def test_global_merge_shortcut(settings):
    settings.set("MERGE_ENABLED_FOR_DYNACONF", True)
    settings.set("MERGE_KEY", ["item1"])
    settings.set("MERGE_KEY", ["item1"])
    assert settings.MERGE_KEY == ["item1"]


def test_local_set_merge_dict():
    settings = Dynaconf()
    settings.set("DATABASE", {"host": "localhost", "port": 666})
    # calling twice  does not change anything
    settings.set("DATABASE", {"host": "localhost", "port": 666})
    assert settings.DATABASE == {"host": "localhost", "port": 666}

    settings.set(
        "DATABASE", {"host": "new", "user": "admin", "dynaconf_merge": True}
    )
    assert settings.DATABASE == {"host": "new", "port": 666, "user": "admin"}
    assert settings.DATABASE.HOST == "new"
    assert settings.DATABASE.user == "admin"

    settings.set("DATABASE.attrs", {"a": ["b", "c"]})
    settings.set("DATABASE.attrs", {"dynaconf_merge": {"foo": "bar"}})
    assert settings.DATABASE.attrs == {"a": ["b", "c"], "foo": "bar"}

    settings.set("DATABASE.attrs", {"yeah": "baby", "dynaconf_merge": True})
    assert settings.DATABASE.attrs == {
        "a": ["b", "c"],
        "foo": "bar",
        "yeah": "baby",
    }

    settings.set(
        "DATABASE.attrs",
        {"a": ["d", "e", "dynaconf_merge"], "dynaconf_merge": True},
    )

    assert settings.DATABASE.attrs == {
        "a": ["b", "c", "d", "e"],
        "foo": "bar",
        "yeah": "baby",
    }

    settings.set("DATABASE.attrs.a", ["d", "e", "f", "dynaconf_merge_unique"])
    assert settings.DATABASE.attrs.a == ["b", "c", "d", "e", "f"]


def test_local_set_merge_list(settings):
    settings.set("PLUGINS", ["core"])
    settings.set("PLUGINS", ["core"])
    assert settings.PLUGINS == ["core"]

    settings.set("PLUGINS", ["debug_toolbar", "dynaconf_merge"])
    assert settings.PLUGINS == ["core", "debug_toolbar"]


def test_local_set_merge_list_unique(settings):
    settings.set("SCRIPTS", ["install.sh", "deploy.sh"])
    settings.set("SCRIPTS", ["install.sh", "deploy.sh"])
    assert settings.SCRIPTS == ["install.sh", "deploy.sh"]

    settings.set(
        "SCRIPTS", ["dev.sh", "test.sh", "deploy.sh", "dynaconf_merge_unique"]
    )
    assert settings.SCRIPTS == ["install.sh", "dev.sh", "test.sh", "deploy.sh"]


def test_local_set_merge_false_dict():
    # environment = False
    settings_a = Dynaconf(
        environments=False,
        merge_enabled=True,
    )
    dict_old = {"a": {"b": {"key_old": "from_old"}}}
    dict_new = {"a": {"b": {"key_new": "from_new", "dynaconf_merge": False}}}
    settings_a.set("default", {"dicty": dict_old, "listy": [1, 2, 3]})
    settings_a.set("default", {"dicty": dict_new, "listy": [9999]})

    assert settings_a.default.dicty.a.b == {"key_new": "from_new"}
    assert settings_a.default.listy == [1, 2, 3, 9999]

    # environment = True
    settings_b = Dynaconf(
        environments=True,
        merge_enabled=True,
    )
    dict_old = {"a": {"b": {"key_old": "from_old"}}}
    dict_new = {"a": {"b": {"key_new": "from_new", "dynaconf_merge": False}}}
    settings_b.set("test", {"dicty": dict_old, "listy": [1, 2, 3]})
    settings_b.set("test", {"dicty": dict_new, "listy": [9999]})
    assert settings_b.test.dicty.a.b == {"key_new": "from_new"}
    assert settings_b.test.listy == [1, 2, 3, 9999]


def test_set_explicit_merge_token(tmpdir):
    data = {
        "a_list": [1, 2],
        "b_list": [1],
        "a_dict": {"name": "Bruno"},
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data, merge=False)
    settings = LazySettings(settings_file="settings.toml")
    assert settings.A_LIST == [1, 2]
    assert settings.B_LIST == [1]
    assert settings.A_DICT == {"name": "Bruno"}
    assert settings.A_DICT.name == "Bruno"

    settings.set("a_list", [3], merge=True)
    assert settings.A_LIST == [1, 2, 3]

    settings.set("b_list", "@merge [2]")
    assert settings.B_LIST == [1, 2]

    settings.set("b_list", "@merge [3, 4]")
    assert settings.B_LIST == [1, 2, 3, 4]

    settings.set("b_list", "@merge 5")
    assert settings.B_LIST == [1, 2, 3, 4, 5]

    settings.set("b_list", "@merge 6.6")
    assert settings.B_LIST == [1, 2, 3, 4, 5, 6.6]

    settings.set("b_list", "@merge false")
    assert settings.B_LIST == [1, 2, 3, 4, 5, 6.6, False]

    settings.set("b_list", "@merge foo,bar")
    assert settings.B_LIST == [1, 2, 3, 4, 5, 6.6, False, "foo", "bar"]

    settings.set("b_list", "@merge zaz")
    assert settings.B_LIST == [1, 2, 3, 4, 5, 6.6, False, "foo", "bar", "zaz"]

    settings.set("b_list", "@merge 7, 8, 9")
    assert settings.B_LIST == [
        1,
        2,
        3,
        4,
        5,
        6.6,
        False,
        "foo",
        "bar",
        "zaz",
        7,
        8,
        9,
    ]
    settings.set("b_list", "@merge '10','11','12'")
    assert settings.B_LIST == [
        1,
        2,
        3,
        4,
        5,
        6.6,
        False,
        "foo",
        "bar",
        "zaz",
        7,
        8,
        9,
        "10",
        "11",
        "12",
    ]

    settings.set("a_dict", "@merge {city='Guarulhos'}")
    assert settings.A_DICT.name == "Bruno"
    assert settings.A_DICT.city == "Guarulhos"

    settings.set("a_dict", "@merge country=Brasil")
    assert settings.A_DICT.name == "Bruno"
    assert settings.A_DICT.city == "Guarulhos"
    assert settings.A_DICT.country == "Brasil"

    settings.set("new_key", "@merge foo=bar")
    assert settings.NEW_KEY == {"foo": "bar"}


def test_set_new_merge_issue_241_1(tmpdir):
    data = {
        "default": {
            "name": "Bruno",
            "colors": ["red", "green"],
            "data": {
                "links": {"twitter": "rochacbruno", "site": "brunorocha.org"}
            },
        }
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data, merge=False)
    settings = LazySettings(environments=True, settings_file="settings.toml")
    assert settings.NAME == "Bruno"
    assert settings.COLORS == ["red", "green"]
    assert settings.DATA.links == {
        "twitter": "rochacbruno",
        "site": "brunorocha.org",
    }


def test_set_new_merge_issue_241_2(tmpdir):
    data = {
        "default": {
            "name": "Bruno",
            "colors": ["red", "green"],
            "data": {
                "links": {"twitter": "rochacbruno", "site": "brunorocha.org"}
            },
        }
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data, merge=False)

    data = {
        "dynaconf_merge": True,
        "default": {
            "colors": ["blue"],
            "data": {"links": {"github": "rochacbruno.github.io"}},
        },
    }
    toml_loader.write(
        str(tmpdir.join("settings.local.toml")), data, merge=False
    )

    settings = LazySettings(environments=True, settings_file="settings.toml")
    assert settings.NAME == "Bruno"
    assert settings.COLORS == ["red", "green", "blue"]
    assert settings.DATA.links == {
        "twitter": "rochacbruno",
        "site": "brunorocha.org",
        "github": "rochacbruno.github.io",
    }


def test_set_new_merge_issue_241_3(tmpdir):
    data = {
        "name": "Bruno",
        "colors": ["red", "green"],
        "data": {
            "links": {"twitter": "rochacbruno", "site": "brunorocha.org"}
        },
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data, merge=False)

    data = {
        "name": "Tommy Shelby",
        "colors": {"dynaconf_merge": ["yellow", "pink"]},
        "data": {"links": {"site": "pb.com"}},
    }
    toml_loader.write(
        str(tmpdir.join("settings.local.toml")), data, merge=False
    )

    settings = LazySettings(settings_file="settings.toml")
    assert settings.NAME == "Tommy Shelby"
    assert settings.COLORS == ["red", "green", "yellow", "pink"]
    assert settings.DATA.links == {"site": "pb.com"}


def test_set_new_merge_issue_241_4(tmpdir):
    data = {
        "name": "Bruno",
        "colors": ["red", "green"],
        "data": {
            "links": {"twitter": "rochacbruno", "site": "brunorocha.org"}
        },
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data, merge=False)

    data = {"data__links__telegram": "t.me/rochacbruno"}
    toml_loader.write(
        str(tmpdir.join("settings.local.toml")), data, merge=False
    )

    settings = LazySettings(settings_file="settings.toml")
    assert settings.NAME == "Bruno"
    assert settings.COLORS == ["red", "green"]
    assert settings.DATA.links == {
        "twitter": "rochacbruno",
        "site": "brunorocha.org",
        "telegram": "t.me/rochacbruno",
    }


def test_set_new_merge_issue_241_5(tmpdir):
    data = {
        "default": {
            "name": "Bruno",
            "colors": ["red", "green"],
            "data": {
                "links": {"twitter": "rochacbruno", "site": "brunorocha.org"}
            },
        }
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data, merge=False)

    data = {"default": {"colors": "@merge ['blue']"}}
    toml_loader.write(
        str(tmpdir.join("settings.local.toml")), data, merge=False
    )

    settings = LazySettings(environments=True, settings_file="settings.toml")
    assert settings.NAME == "Bruno"
    assert settings.COLORS == ["red", "green", "blue"]
    assert settings.DATA.links == {
        "twitter": "rochacbruno",
        "site": "brunorocha.org",
    }


def test_set_insert_token(tmpdir):
    data = {
        "a_list": [1, 2],
        "b_list": [1],
        "a_dict": {"name": "Bruno", "teams": ["dev"]},
        "list_of_dicts": [{"number": 1}],
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data, merge=False)
    settings = LazySettings(settings_file="settings.toml")
    assert settings.A_LIST == [1, 2]
    assert settings.B_LIST == [1]
    assert settings.A_DICT == {"name": "Bruno", "teams": ["dev"]}
    assert settings.A_DICT.name == "Bruno"
    assert settings.A_DICT.teams == ["dev"]

    settings.set("a_list", "@insert 0 0")
    assert settings.A_LIST == [0, 1, 2]

    settings.set("a_list", "@insert 1 1.5")
    assert settings.A_LIST == [0, 1.5, 1, 2]

    settings.set("a_list", "@insert 4 4")
    assert settings.A_LIST == [0, 1.5, 1, 2, 4]

    settings.set("b_list", "@insert 0 42")
    assert settings.B_LIST == [42, 1]

    settings.set("b_list", "@insert 1 43")
    assert settings.B_LIST == [42, 43, 1]

    settings.set("a_dict.teams", "@insert 0 'admin'")
    assert settings.A_DICT.teams == ["admin", "dev"]

    settings.set("a_dict.teams", "@insert 1 'staff'")
    assert settings.A_DICT.teams == ["admin", "staff", "dev"]

    settings.set("a_dict.teams", "@insert 3 'user'")
    assert settings.A_DICT.teams == ["admin", "staff", "dev", "user"]

    settings.set("a_dict.teams", "@insert 0 'admin'")
    assert settings.A_DICT.teams == ["admin", "admin", "staff", "dev", "user"]

    settings.set("new_key", "@insert 0 'foo'")
    assert settings.NEW_KEY == ["foo"]

    settings.set("new_key", "@insert 'bar'")
    assert settings.NEW_KEY == ["bar", "foo"]

    settings.set("new_key", "@insert 42")
    assert settings.NEW_KEY == [42, "bar", "foo"]

    settings.set("new_key", "@insert -1 'last'")
    assert settings.NEW_KEY == [42, "bar", "last", "foo"]

    settings.set("list_of_dicts", '@insert 0 @json {"number": 0}')
    assert settings.LIST_OF_DICTS == [{"number": 0}, {"number": 1}]

    settings.set("list_of_dicts", "@insert 2 {number=2}")  # valid toml
    assert settings.LIST_OF_DICTS == [
        {"number": 0},
        {"number": 1},
        {"number": 2},
    ]


def test_set_with_non_str_types():
    """This replicates issue #1005 in a simplified setup."""
    settings = Dynaconf(merge_enabled=True)
    settings.set("a", {"b": {1: "foo"}})
    settings.set("a", {"b": {"c": "bar"}})
    assert settings["a"]["b"][1] == "foo"
    assert settings["a"]["b"]["c"] == "bar"


def test_set_with_non_str_types_on_first_level():
    """Non-str key types on first level."""
    settings = Dynaconf(merge_enabled=True)
    settings.set(1, {"b": {1: "foo"}})
    settings.set("a", {"1": {1: "foo"}})
    assert settings[1]["b"][1] == "foo"
    assert settings[1].b[1] == "foo"
    assert settings.get(1).b[1] == "foo"
    assert settings["a"]["1"][1] == "foo"


def test_exists(settings):
    settings.set("BOOK", "TAOCP")
    assert settings.exists("BOOK") is True


def test_dotted_traversal_access(settings):
    settings.set(
        "PARAMS",
        {
            "PASSWORD": "secret",
            "SSL": {"CONTEXT": "SECURE"},
            "DOTTED.KEY": True,
        },
        dotted_lookup=False,
    )
    assert settings.get("PARAMS") == {
        "PASSWORD": "secret",
        "SSL": {"CONTEXT": "SECURE"},
        "DOTTED.KEY": True,
    }

    assert settings("PARAMS.PASSWORD") == "secret"
    assert settings("PARAMS.SSL.CONTEXT") == "SECURE"
    assert settings("PARAMS.TOKEN.IMAGINARY", 1234) == 1234
    assert settings("IMAGINARY_KEY.FOO") is None
    assert settings("IMAGINARY_KEY") is None

    assert settings["PARAMS.PASSWORD"] == "secret"
    assert settings["PARAMS.SSL.CONTEXT"] == "SECURE"
    assert settings.PARAMS.SSL.CONTEXT == "SECURE"

    assert settings.exists("PARAMS") is True
    assert settings.exists("PARAMS.PASSWORD") is True
    assert settings.exists("PARAMS.SSL") is True
    assert settings.exists("PARAMS.SSL.FAKE") is False
    assert settings.exists("PARAMS.SSL.CONTEXT") is True

    # Dotted traversal should not work for dictionary-like key access.
    with pytest.raises(KeyError):
        settings["PARAMS.DOESNOTEXIST"]

    # Disable dot-traversal on a per-call basis.
    assert settings("PARAMS.PASSWORD", dotted_lookup=False) is None

    assert settings("PARAMS.DOTTED.KEY") is None
    assert settings("PARAMS").get("DOTTED.KEY") is True

    settings.set("DOTTED.KEY", True, dotted_lookup=False)
    assert settings("DOTTED.KEY", dotted_lookup=False) is True

    settings.set("NESTED_1", {"nested_2": {"nested_3": {"nested_4": True}}})

    assert settings.NESTED_1.nested_2.nested_3.nested_4 is True
    assert settings["NESTED_1.nested_2.nested_3.nested_4"] is True
    assert settings("NESTED_1.nested_2.nested_3.nested_4") is True
    # First key is always transformed to upper()
    assert settings("nested_1.nested_2.nested_3.nested_4") is True

    # using cast
    settings.set("me.name", '@json ["bruno", "rocha"]')
    settings.set("me.number", "42")
    assert settings.get("me.name", cast=True, default=["bruno", "rocha"]) == [
        "bruno",
        "rocha",
    ]
    assert settings.get("me.number", cast="@int", default=42) == 42

    # nested separator test
    assert settings.get("ME__NUMBER") == "42"


def test_dotted_set(settings):
    settings.set("MERGE_ENABLED_FOR_DYNACONF", False)

    settings.set("nested_1.nested_2.nested_3.nested_4", "secret")

    assert settings.NESTED_1.NESTED_2.NESTED_3.NESTED_4 == "secret"
    assert settings.NESTED_1.NESTED_2.NESTED_3.to_dict() == {
        "nested_4": "secret"
    }
    assert settings.NESTED_1.NESTED_2.to_dict() == {
        "nested_3": {"nested_4": "secret"}
    }

    assert settings.get("nested_1").to_dict() == {
        "nested_2": {"nested_3": {"nested_4": "secret"}}
    }

    with pytest.raises(KeyError):
        settings.NESTED_1.NESTED_2_0

    settings.set("nested_1.nested_2_0", "Hello")
    assert settings.NESTED_1.NESTED_2_0 == "Hello"

    settings.set("nested_1.nested_2.nested_3.nested_4", "Updated Secret")
    assert settings.NESTED_1.NESTED_2.NESTED_3.NESTED_4 == "Updated Secret"
    assert settings.NESTED_1.NESTED_2.NESTED_3.to_dict() == {
        "nested_4": "Updated Secret"
    }
    assert settings.NESTED_1.NESTED_2.to_dict() == {
        "nested_3": {"nested_4": "Updated Secret"}
    }

    assert settings.get("nested_1").to_dict() == {
        "nested_2": {"nested_3": {"nested_4": "Updated Secret"}},
        "nested_2_0": "Hello",
    }


def test_dotted_set_with_indexing(settings):
    settings.set("MERGE_ENABLED_FOR_DYNACONF", False)

    # Dotted set with index
    settings.set("nested_a.nested_b[2][1].nested_c.nested_d[3]", "old_conf")
    settings.set(
        "nested_a.nested_b[2][1].nested_c.nested_d[3]", "new_conf1"
    )  # overwrite
    settings.set(
        "nested_a.nested_b[2][1].nested_c.nested_d[2]", "new_conf2"
    )  # insert
    assert settings.NESTED_A.NESTED_B[2][1].NESTED_C.NESTED_D[3] == "new_conf1"
    assert settings.NESTED_A.NESTED_B[2][1].NESTED_C.NESTED_D[2] == "new_conf2"
    assert len(settings.NESTED_A.NESTED_B[0]) < 1
    settings.set(
        "nested_a.nested_b[0][2].nested_c.nested_d[0]", "extra_conf"
    )  # add more
    assert len(settings.NESTED_A.NESTED_B[0]) > 0
    settings.set(
        "nested_a.nested_b[2][1].nested_c.nested_d",
        ["conf1", "conf2", "conf3"],
    )  # overwrite list
    assert settings.NESTED_A.NESTED_B[2][1].NESTED_C.NESTED_D == [
        "conf1",
        "conf2",
        "conf3",
    ]

    # This test case is the reason why choosing
    # __(\d+) pattern instead of _(\d+)_
    settings.set("nested_5.nested_6_0", "World")
    assert settings.NESTED_5.NESTED_6_0 == "World"


def test_dotted_set_with_merge(settings):
    settings.set("MERGE_ENABLED_FOR_DYNACONF", False)

    start_data = {
        "default": {
            "NAME": "testdb",
            "ENGINE": "db.foo.bar",
            "PORT": 6666,
            "PARAMS": ["a", "b", "c"],
            "ATTRS": {"a": 1, "b": 2},
        }
    }
    settings.set("DATABASES", start_data)

    assert settings.DATABASES == start_data

    # Change DB name
    settings.set("DATABASES.default.NAME", "bladb")
    assert settings.DATABASES != start_data
    assert settings.DATABASES["default"].keys() == start_data["default"].keys()
    settings.DATABASES.default.NAME == "bladb"

    # Replace items on a list
    assert settings.DATABASES.default.PARAMS == ["a", "b", "c"]
    settings.set("DATABASES.default.PARAMS", ["d", "e"])
    assert settings.DATABASES != start_data
    assert settings.DATABASES["default"].keys() == start_data["default"].keys()
    assert settings.DATABASES.default.PARAMS == ["d", "e"]

    # Add new items to the list
    settings.set("DATABASES.default.PARAMS", '@merge ["e", "f", "g"]')
    assert settings.DATABASES != start_data
    assert settings.DATABASES["default"].keys() == start_data["default"].keys()
    assert settings.DATABASES.default.PARAMS == ["d", "e", "e", "f", "g"]

    # Replace a dict
    assert settings.DATABASES.default.ATTRS == {"a": 1, "b": 2}
    settings.set("DATABASES.default.ATTRS", {"c": 3})
    assert settings.DATABASES != start_data
    assert settings.DATABASES["default"].keys() == start_data["default"].keys()
    assert settings.DATABASES.default.ATTRS == {"c": 3}

    # Add new item to the dict
    settings.set("DATABASES.default.ATTRS", '@merge {"b": 2, "d": 4}')
    assert settings.DATABASES != start_data
    assert settings.DATABASES["default"].keys() == start_data["default"].keys()
    assert settings.DATABASES.default.ATTRS == {"b": 2, "c": 3, "d": 4}

    # Replace the entire list
    settings.set("DATABASES.default.PARAMS", ["x", "y", "z"], tomlfy=True)
    assert settings.DATABASES != start_data
    assert settings.DATABASES["default"].keys() == start_data["default"].keys()
    assert settings.DATABASES.default.PARAMS == ["x", "y", "z"]

    # Replace the entire dict
    settings.set("DATABASES.default.ATTRS", "{x=26}", tomlfy=True)
    assert settings.DATABASES != start_data
    assert settings.DATABASES["default"].keys() == start_data["default"].keys()
    assert settings.DATABASES.default.ATTRS == {"x": 26}


def test_deepcopy_settings():
    import copy

    from dynaconf.base import Settings

    s1 = Settings()
    s2 = copy.deepcopy(s1)
    assert id(s1) != id(s2)
    assert id(s1.__core__) != id(s2.__core__)
    assert s1.__core__.id != s2.__core__.id


def test_from_env_method(clean_env, tmpdir):
    data = {
        "default": {"a_default": "From default env"},
        "development": {
            "value": "From development env",
            "only_in_development": True,
        },
        "other": {"value": "From other env", "only_in_other": True},
    }
    toml_path = str(tmpdir.join("base_settings.toml"))
    toml_loader.write(toml_path, data, merge=False)
    settings = LazySettings(settings_file=toml_path, environments=True)
    settings.set("ARBITRARY_KEY", "arbitrary value")

    assert settings.VALUE == "From development env"
    assert settings.A_DEFAULT == "From default env"
    assert settings.ONLY_IN_DEVELOPMENT is True
    assert settings.ARBITRARY_KEY == "arbitrary value"
    assert settings.get("ONLY_IN_OTHER") is None

    # clone the settings object pointing to a new env
    other_settings = settings.from_env("other")
    assert other_settings.VALUE == "From other env"
    assert other_settings.A_DEFAULT == "From default env"
    assert other_settings.ONLY_IN_OTHER is True
    assert other_settings.get("ARBITRARY_KEY") is None
    assert other_settings.get("ONLY_IN_DEVELOPMENT") is None
    with pytest.raises(AttributeError):
        # values set programmatically are not cloned
        other_settings.ARBITRARY_KEY
    with pytest.raises(AttributeError):
        # values set only in a specific env not cloned
        other_settings.ONLY_IN_DEVELOPMENT
    # assert it is cached not created twice
    assert other_settings is settings.from_env("other")

    # Now the same using keep=True
    other_settings = settings.from_env("other", keep=True)
    assert other_settings.VALUE == "From other env"
    assert other_settings.A_DEFAULT == "From default env"
    assert other_settings.ONLY_IN_OTHER is True
    assert other_settings.ONLY_IN_DEVELOPMENT is True
    assert settings.ARBITRARY_KEY == "arbitrary value"

    # assert it is created not cached
    assert other_settings is not settings.from_env("other")

    # settings remains the same
    assert settings.VALUE == "From development env"
    assert settings.A_DEFAULT == "From default env"
    assert settings.ONLY_IN_DEVELOPMENT is True
    assert settings.ARBITRARY_KEY == "arbitrary value"
    assert settings.get("ONLY_IN_OTHER") is None

    # additional kwargs
    data = {
        "default": {"a_default": "From default env"},
        "production": {"value": "From prod env", "only_in_prod": True},
        "other": {"value": "From other env", "only_in_other": True},
    }
    toml_path = str(tmpdir.join("other_settings.toml"))
    toml_loader.write(toml_path, data, merge=False)

    new_other_settings = other_settings.from_env(
        "production", keep=True, SETTINGS_FILE_FOR_DYNACONF=toml_path
    )

    # production values
    assert new_other_settings.VALUE == "From prod env"
    assert new_other_settings.ONLY_IN_PROD is True
    # keep=True values
    assert new_other_settings.ONLY_IN_OTHER is True
    assert new_other_settings.ONLY_IN_DEVELOPMENT is True
    assert settings.A_DEFAULT == "From default env"


def test_envless_load_file(tmpdir):
    """Ensure passing settings.load_file accepts env argument.

    settings.load_file("filename.toml", env="development")
    will load the default + development env.

    settings.load_file("filename.toml", env=False|None)
    will load in envless mode.

    settings.load_file("filename.toml")
    will respect the current env settings (normal mode).
    """
    data = {
        "default": {"a_default": "From default env"},
        "development": {"value": "From development env"},
    }
    toml_path = str(tmpdir.join("base_settings.toml"))
    toml_loader.write(toml_path, data, merge=False)

    data = {
        "development": {"another_value": "Another development env"},
    }
    toml_path2 = str(tmpdir.join("base_settings2.toml"))
    toml_loader.write(toml_path2, data, merge=False)

    data = {"value": "Envless value"}
    toml_path_envless = str(tmpdir.join("base_settings_envless.toml"))
    toml_loader.write(toml_path_envless, data, merge=False)

    settings = Dynaconf(
        environments=["development", "other"], settings_file=toml_path
    )
    assert settings.VALUE == "From development env"
    assert settings.A_DEFAULT == "From default env"

    # load the env file normal way (respecting current env)
    settings.load_file(toml_path2)
    assert settings.ANOTHER_VALUE == "Another development env"

    # load the envless file
    settings.load_file(toml_path_envless, env=False)
    assert settings.VALUE == "Envless value"


def test_from_env_method_with_prefix(clean_env, tmpdir):
    data = {
        "default": {"prefix_a_default": "From default env"},
        "development": {
            "prefix_value": "From development env",
            "prefix_only_in_development": True,
        },
        "other": {
            "prefix_value": "From other env",
            "prefix_only_in_other": True,
            "not_prefixed": "no prefix",
        },
    }
    toml_path = str(tmpdir.join("base_settings.toml"))
    toml_loader.write(toml_path, data, merge=False)
    settings = LazySettings(
        settings_file=toml_path,
        environments=True,
        filter_strategy=PrefixFilter("prefix"),
    )
    settings.set("ARBITRARY_KEY", "arbitrary value")

    assert settings.VALUE == "From development env"
    assert settings.A_DEFAULT == "From default env"
    assert settings.ONLY_IN_DEVELOPMENT is True
    assert settings.ARBITRARY_KEY == "arbitrary value"
    assert settings.get("ONLY_IN_OTHER") is None

    # clone the settings object pointing to a new env
    other_settings = settings.from_env("other")
    assert other_settings.VALUE == "From other env"
    assert other_settings.A_DEFAULT == "From default env"
    assert other_settings.ONLY_IN_OTHER is True
    assert other_settings.get("ARBITRARY_KEY") is None
    assert other_settings.get("ONLY_IN_DEVELOPMENT") is None
    with pytest.raises(AttributeError):
        other_settings.not_prefixed
    with pytest.raises(AttributeError):
        # values set programmatically are not cloned
        other_settings.ARBITRARY_KEY
    with pytest.raises(AttributeError):
        # values set only in a specific env not cloned
        other_settings.ONLY_IN_DEVELOPMENT
    # assert it is cached not created twice
    assert other_settings is settings.from_env("other")


def test_preload(tmpdir):
    data = {
        "data": {"links": {"twitter": "rochacbruno", "site": "brunorocha.org"}}
    }
    toml_loader.write(str(tmpdir.join("preload.toml")), data, merge=False)

    data = {
        "dynaconf_merge": True,
        "data": {"links": {"github": "rochacbruno.github.io"}},
    }
    toml_loader.write(
        str(tmpdir.join("main_settings.toml")), data, merge=False
    )

    data = {
        "dynaconf_merge": True,
        "data": {"links": {"mastodon": "mastodon.social/@rochacbruno"}},
    }
    toml_loader.write(str(tmpdir.join("included.toml")), data, merge=False)

    settings = LazySettings(
        PRELOAD_FOR_DYNACONF=["preload.toml"],
        SETTINGS_FILE_FOR_DYNACONF="main_settings.toml",
        INCLUDES_FOR_DYNACONF=["included.toml"],
    )

    assert settings.DATA.links == {
        "twitter": "rochacbruno",
        "site": "brunorocha.org",
        "github": "rochacbruno.github.io",
        "mastodon": "mastodon.social/@rochacbruno",
    }


def test_config_aliases(tmpdir):
    data = {
        "hello": {"name": "Bruno", "passwd": 1234},
        "awesome": {"passwd": 5678},
    }
    toml_loader.write(str(tmpdir.join("blarg.toml")), data, merge=False)

    settings = LazySettings(
        envvar_prefix="BRUCE",
        core_loaders=["TOML"],
        loaders=["dynaconf.loaders.env_loader"],
        default_env="hello",
        env_switcher="BRUCE_ENV",
        prelaod=[],
        settings_file=["blarg.toml"],
        includes=[],
        ENV="awesome",
        environments=True,
    )
    config = settings.__core__.config

    assert settings.NAME == "Bruno"
    assert settings.PASSWD == 5678

    assert settings.ENVVAR_PREFIX_FOR_DYNACONF == "BRUCE"
    assert settings.CORE_LOADERS_FOR_DYNACONF == ["TOML"]
    assert settings.LOADERS_FOR_DYNACONF == ["dynaconf.loaders.env_loader"]
    assert len(config.loaders) == 1
    assert settings.DEFAULT_ENV_FOR_DYNACONF == "hello"
    assert settings.ENV_SWITCHER_FOR_DYNACONF == "BRUCE_ENV"
    assert settings.PRELOAD_FOR_DYNACONF == []
    assert settings.SETTINGS_FILE_FOR_DYNACONF == ["blarg.toml"]
    assert settings.INCLUDES_FOR_DYNACONF == []
    assert settings.ENV_FOR_DYNACONF == "awesome"
    assert settings.current_env == "awesome"


def test_envless_mode(tmpdir):
    data = {
        "foo": "bar",
        "hello": "world",
        "default": 1,
        "databases": {"default": {"port": 8080}},
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data)

    settings = LazySettings(
        settings_file="settings.toml"
    )  # already the default
    assert settings.FOO == "bar"
    assert settings.HELLO == "world"
    assert settings.DEFAULT == 1
    assert settings.DATABASES.default.port == 8080


def test_envless_mode_with_prefix(tmpdir):
    data = {
        "prefix_foo": "bar",
        "hello": "world",
        "prefix_default": 1,
        "prefix_databases": {"default": {"port": 8080}},
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data)

    settings = LazySettings(
        settings_file="settings.toml", filter_strategy=PrefixFilter("prefix")
    )  # already the default
    assert settings.FOO == "bar"
    with pytest.raises(AttributeError):
        settings.HELLO
    assert settings.DEFAULT == 1
    assert settings.DATABASES.default.port == 8080


def test_lowercase_read_mode(tmpdir):
    """
    Starting on 3.0.0 lowercase keys are enabled by default
    """
    data = {
        "foo": "bar",
        "hello": "world",
        "default": 1,
        "databases": {"default": {"port": 8080}},
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data)

    # settings_files misspelled.. should be `settings_file`
    settings = LazySettings(settings_files="settings.toml")

    assert settings.FOO == "bar"
    assert settings.foo == "bar"
    assert settings.HELLO == "world"
    assert settings.hello == "world"
    assert settings.DEFAULT == 1
    assert settings.default == 1
    assert settings.DATABASES.default.port == 8080
    assert settings.databases.default.port == 8080

    assert "foo" in settings
    assert "FOO" in settings

    # test __dir__
    results = dir(settings)
    assert "foo" in results
    assert "FOO" in results

    results = dir(settings.databases)
    assert "default" in results
    assert "DEFAULT" in results


def test_settings_dict_like_iteration(tmpdir):
    """Settings can be iterated just like a dict"""
    data = {
        "foo": "bar",
        "hello": "world",
        "default": 1,
        "databases": {"default": {"port": 8080}},
    }
    toml_loader.write(str(tmpdir.join("settings.toml")), data)

    # settings_files misspelled.. should be `settings_file`
    settings = LazySettings(settings_files="settings.toml")

    for key in settings:
        assert key in settings.store

    for key, value in settings.items():
        assert settings.store[key] == value


def test_prefix_is_not_str_raises():
    with pytest.raises(TypeError):
        toml_loader.load(LazySettings(filter_strategy=PrefixFilter(int)))
    with pytest.raises(TypeError):
        toml_loader.load(LazySettings(filter_strategy=PrefixFilter(True)))


def test_clone():
    # create a settings object
    settings = LazySettings(FOO="bar")
    assert settings.FOO == "bar"

    # clone it
    cloned = settings.dynaconf.clone()
    assert cloned.FOO == "bar"

    # modify the cloned settings
    cloned.FOO = "baz"
    assert cloned.FOO == "baz"

    # assert original settings is not modified
    assert settings.FOO == "bar"


def test_clone_with_module_type():
    # create a settings object
    settings = LazySettings(FOO="bar", A_MODULE=os)
    # adding a module type makes object unpickaable
    # then .clone raised an error, this was fixed by copying the dict.
    assert settings.FOO == "bar"

    # clone it
    cloned = settings.dynaconf.clone()
    assert cloned.FOO == "bar"

    # modify the cloned settings
    cloned.FOO = "baz"
    assert cloned.FOO == "baz"

    # assert original settings is not modified
    assert settings.FOO == "bar"

    assert settings.A_MODULE == cloned.A_MODULE


def test_wrap_existing_settings():
    """
    Wrap an existing settings object
    """
    settings = LazySettings(FOO="bar")
    assert settings.FOO == "bar"

    # wrap it
    wrapped = LazySettings(settings._wrapped)
    assert wrapped.FOO == "bar"

    # modify the wrapped settings
    wrapped.FOO = "baz"
    assert wrapped.FOO == "baz"

    # assert original settings is also modified as they have the same wrapped
    assert settings.FOO == "baz"


def test_wrap_existing_settings_clone():
    """
    Wrap an existing settings object
    """
    settings = LazySettings(FOO="bar")
    assert settings.FOO == "bar"

    # wrap it
    wrapped = LazySettings(settings.dynaconf.clone())
    assert wrapped.FOO == "bar"

    # modify the wrapped settings
    wrapped.FOO = "baz"
    assert wrapped.FOO == "baz"

    # assert original settings is not changes as we used a wrapped clone
    assert settings.FOO == "bar"


def test_list_entries_from_yaml_should_not_duplicate_when_merged(tmpdir):
    data = {
        "default": {
            "SOME_KEY": "value",
            "SOME_LIST": ["item_1", "item_2", "item_3"],
        },
        "other": {"SOME_KEY": "new_value", "SOME_LIST": ["item_4", "item_5"]},
    }
    yaml_loader.write(str(tmpdir.join("test_settings.yaml")), data)

    settings = Dynaconf(
        settings_files="test_settings.yaml",
        environments=True,
        merge_enabled=True,
    )

    expected_default_value = DataList(["item_1", "item_2", "item_3"])
    expected_other_value = DataList(
        ["item_1", "item_2", "item_3", "item_4", "item_5"]
    )

    assert settings.from_env("default").SOME_LIST == expected_default_value
    assert settings.from_env("other").SOME_LIST == expected_other_value


# #712
# introduce update(validate), set(validate) and load_file(validate)
# and global VALIDATE_ON_UPDATE option

# parametrize data to tests
validate_on_update_data = (
    pytest.param(
        {"value_a": "foo"},  # valid data
        {"value_b": "bar"},  # invalid data
        id="simple-value",
    ),
    pytest.param(
        {"value_a__nested": "foo"},
        {"value_b__nested": "bar"},
        id="dunder-value",
    ),
)


@pytest.mark.parametrize("valid_data,invalid_data", validate_on_update_data)
def test_update__validate_on_update_is_false(valid_data, invalid_data):
    """
    When `Dynaconf(validate_on_update=False)`
    Should behave correctly (bypass, pass or raise)
    """
    settings = Dynaconf()  # validate_on_update default is false
    settings.validators.register(Validator("value_a", must_exist=True))

    # should bypass
    settings.update(invalid_data)
    settings.update(invalid_data, validate="random")
    settings.update(invalid_data, validate=123)

    # should raise
    with pytest.raises(ValidationError):
        settings.update(invalid_data, validate=True)

    # should pass
    assert not settings.exists("value_a")
    settings.update(valid_data, validate=True)


@pytest.mark.parametrize("valid_data,invalid_data", validate_on_update_data)
def test_update__validate_on_update_is_true(valid_data, invalid_data):
    """
    When `Dynaconf(validate_on_update=True)`
    Should behave correctly (bypass, pass or raise)
    """
    settings = Dynaconf(validate_on_update=True)
    settings.validators.register(Validator("value_a", must_exist=True))

    # should bypass
    settings.update(invalid_data, validate=False)
    settings.update(invalid_data, validate="random")
    settings.update(invalid_data, validate=123)

    # should raise
    with pytest.raises(ValidationError):
        settings.update(invalid_data)

    # should pass
    assert not settings.exists("value_a")
    settings.update(valid_data)


def test_update__validate_on_update_is_str_all():
    """
    When `Dynaconf(validate_on_update="all")`
    Should behave correctly (bypass, pass or raise)
    """
    settings = Dynaconf(validate_on_update="all")
    settings.validators.register(Validator("value_a", must_exist=True))
    settings.validators.register(Validator("value_int", eq=42))

    # should bypass
    settings.update({"value_b": "foo"}, validate=False)
    settings.update({"value_b": "foo"}, validate="random")
    settings.update({"value_b": "foo"}, validate=123)

    # should raise and accumulate errors
    with pytest.raises(ValidationError) as e:
        settings.update({"value_b": "not_a", "value_int": 41})

    assert len(e.value.details) == 2

    # should pass
    assert not settings.exists("value_a")
    settings.update({"value_a": "exists", "value_int": 42})


def test_set__validate_on_update_is_false():
    """
    When `Dynaconf(validate_on_update=False)`
    Should behave correctly (bypass, pass or raise)
    """
    settings = Dynaconf()  # validate = false
    settings.validators.register(Validator("value_a", must_exist=True))

    # should bypass
    settings.set("value_b", "foo")
    settings.set("value_b", "foo", validate="random")
    settings.set("value_b", "foo", validate=123)

    # should raise
    with pytest.raises(ValidationError):
        settings.set("value_b", "foo", validate=True)

    # should pass
    assert not settings.exists("value_a")
    settings.set("value_a", "foo", validate=True)


def test_set__validate_on_update_is_true():
    """
    When `Dynaconf(validate_on_update=True)`
    Should behave correctly (bypass, pass or raise)
    """
    settings = Dynaconf(validate_on_update=True)
    settings.validators.register(Validator("value_a", must_exist=True))

    # should bypass
    settings.set("value_b__nested", "foo", validate=False)
    settings.set("value_b__nested", "foo", validate="random")
    settings.set("value_b__nested", "foo", validate=123)

    # should raise
    with pytest.raises(ValidationError):
        settings.set("value_b__nested", "foo")

    # should pass
    assert not settings.exists("value_a")
    settings.set("value_a__nested", "foo")


def test_set__validate_on_update_is_str_all():
    """
    When `Dynaconf(validate_on_update="all")`
    Should behave correctly (bypass, pass or raise)
    """
    settings = Dynaconf(validate_on_update="all")
    settings.validators.register(Validator("value_a", must_exist=True))

    # should bypass
    settings.set("value_b__nested", "foo", validate=False)
    settings.set("value_b__nested", "foo", validate="random")
    settings.set("value_b__nested", "foo", validate=123)

    # should raise. "all" doesn't make difference here
    with pytest.raises(ValidationError):
        settings.set("value_b__nested", "foo")

    # should pass
    assert not settings.exists("value_a")
    settings.set("value_a__nested", "foo")


def mkfile(tmp_dir, filename: str, data: str):
    """
    Test utility function to create tmp files
    """
    file = tmp_dir.join(filename)
    with open(file.strpath, "w", encoding="utf-8") as f:
        f.write(data)
    return file


params__load_file__data = (
    pytest.param(
        {"fname": "valid.toml", "data": "value_a='foo'\nvalue_int=42"},
        {"fname": "invalid.toml", "data": "value_b='foo'\nvalue_int=41"},
        id="load-toml",
    ),
    pytest.param(
        {"fname": "valid.ini", "data": "value_a='foo'\nvalue_int='@int 42'"},
        {"fname": "invalid.ini", "data": "value_b='foo'\nvalue_int='@int 41'"},
        id="load-ini",
    ),
    pytest.param(
        {"fname": "valid.yaml", "data": "value_a: 'foo'\nvalue_int: 42"},
        {"fname": "invalid.yaml", "data": "value_b: 'foo'\nvalue_int: 41"},
        id="load-yaml",
    ),
    pytest.param(
        {"fname": "valid.json", "data": '{"value_a":"foo",\n"value_int":42}'},
        {
            "fname": "invalid.json",
            "data": '{"value_b":"foo",\n"value_int":41}',
        },
        id="load-json",
    ),
    pytest.param(
        {"fname": "valid.py", "data": 'VALUE_A="foo"\nVALUE_INT=42'},
        {"fname": "invalid.py", "data": 'VALUE_B="foo"\nVALUE_INT=41'},
        id="load-py",
    ),
)


@pytest.mark.parametrize("valid,invalid", params__load_file__data)
def test_load_file__validate_on_update_is_false(tmpdir, valid, invalid):
    """
    When `Dynaconf(validate_on_update=False)`
    Should behave correctly (bypass, pass or raise)
    """
    # setup files
    file_with_valid = mkfile(tmpdir, valid["fname"], valid["data"])
    file_with_invalid = mkfile(tmpdir, invalid["fname"], invalid["data"])

    # setup dyna
    settings = Dynaconf()  # validate = false
    settings.validators.register(Validator("value_a", must_exist=True))

    # should bypass when
    settings.load_file(file_with_invalid)
    settings.load_file(file_with_invalid, validate="random")
    settings.load_file(file_with_invalid, validate=123)

    # should raise
    with pytest.raises(ValidationError):
        settings.load_file(file_with_invalid, validate=True)

    # should pass
    assert not settings.exists("value_a")
    settings.load_file(file_with_valid, validate=True)


@pytest.mark.parametrize("valid,invalid", params__load_file__data)
def test_load_file__validate_on_update_is_true(tmpdir, valid, invalid):
    """
    When `Dynaconf(validate_on_update=True)`
    Should behave correctly (bypass, pass or raise)
    """
    # setup files
    file_with_valid = mkfile(tmpdir, valid["fname"], valid["data"])
    file_with_invalid = mkfile(tmpdir, invalid["fname"], invalid["data"])

    # setup dyna
    settings = Dynaconf(validate_on_update=True)
    settings.validators.register(Validator("value_a", must_exist=True))

    # should bypass when
    settings.load_file(file_with_invalid, validate=False)

    # should raise
    with pytest.raises(ValidationError):
        settings.load_file(file_with_invalid)

    # should pass
    assert not settings.exists("value_a")
    settings.load_file(file_with_valid)


@pytest.mark.parametrize("valid,invalid", params__load_file__data)
def test_load_file__validate_on_update_is_str_all(tmpdir, valid, invalid):
    """
    When `Dynaconf(validate_on_update="all")`
    Should behave correctly (bypass, pass or raise accumulating errors)
    """
    # setup files
    file_with_valid = mkfile(tmpdir, valid["fname"], valid["data"])
    file_with_invalid = mkfile(tmpdir, invalid["fname"], invalid["data"])

    # setup dyna
    settings = Dynaconf(validate_on_update="all")
    settings.validators.register(Validator("value_a", must_exist=True))
    settings.validators.register(Validator("value_int", eq=42))

    # should bypass
    settings.load_file(file_with_invalid, validate=False)
    settings.load_file(file_with_invalid, validate="random")
    settings.load_file(file_with_invalid, validate=123)

    # should raise and accumulate errors
    with pytest.raises(ValidationError) as e:
        settings.load_file(file_with_invalid)

    assert len(e.value.details) == 2

    # should pass
    assert not settings.exists("value_a")
    settings.load_file(file_with_valid)


def test_get_with_sysenv_fallback_global_as_false():
    """
    When global sysenv_fallback is False
    Should not fallback to sys envvars (default)
    """
    settings = Dynaconf(sysenv_fallback=False)
    settings.environ["TEST_KEY"] = "TEST_VALUE"

    assert settings.sysenv_fallback_for_dynaconf is False
    assert not settings.get("test_key")


def test_get_with_sysenv_fallback_global_as_true():
    """
    When sysenv_fallback is True
    Should fallback to sys envvars for uppercase envvar-names only
    """
    settings = Dynaconf(sysenv_fallback=True)
    settings.environ["TEST_KEY"] = "TEST_VALUE"

    assert settings.sysenv_fallback_for_dynaconf is True
    assert settings.get("test_key") == "TEST_VALUE"


def test_get_with_sysenv_fallback_global_as_list():
    """
    When sysenv_fallback is list
    Should fallback to sys envvars only for listed names
    """
    settings = Dynaconf(sysenv_fallback=["FOO_KEY"])
    settings.environ["TEST_KEY"] = "TEST_VALUE"
    settings.environ["FOO_KEY"] = "FOO_VALUE"

    assert isinstance(settings.sysenv_fallback_for_dynaconf, list)
    assert not settings.get("test_key")
    assert settings.get("foo_key") == "FOO_VALUE"


def test_get_with_sysenv_fallback_local_overrides():
    """
    When there are local overrides
    Should behave according to the overrides
    """
    # global is False
    settings = Dynaconf(sysenv_fallback=False)
    settings.environ["TEST_KEY"] = "TEST_VALUE"

    assert settings.sysenv_fallback_for_dynaconf is False
    assert not settings.get("test_key")
    assert not settings.get("test_key", sysenv_fallback=["foo_key"])
    assert settings.get("test_key", sysenv_fallback=True) == "TEST_VALUE"
    assert (
        settings.get("test_key", sysenv_fallback=["test_key"]) == "TEST_VALUE"
    )

    # global is True
    settings = Dynaconf(sysenv_fallback=True)
    settings.environ["ANOTHER_TEST"] = "ANOTHER_VALUE"

    assert settings.sysenv_fallback_for_dynaconf is True
    assert settings.get("another_test") == "ANOTHER_VALUE"
    assert not settings.get("another_test", sysenv_fallback=False)


# issue #965
def test_no_extra_values_in_nested_structure():
    settings = Dynaconf()
    settings.set("key", [{"d": "v"}])
    assert settings.key == [{"d": "v"}]


class TestDunderIndexSet:
    def test_environ_dotted_set_with_index(self):
        os.environ["DYNACONF_NESTED_A__nested_1__nested_2"] = "new_conf"
        os.environ[
            "DYNACONF_NESTED_A__nested_b___2___1__nested_c__nested_d___3"
        ] = "old_conf"
        settings = Dynaconf(envvar_prefix="DYANCONF")
        assert isinstance(settings.NESTED_A.NESTED_B, list)
        assert isinstance(settings.NESTED_A.NESTED_B[2], list)
        assert isinstance(settings.NESTED_A.NESTED_B[2][1], dict)
        assert (
            settings.NESTED_A.NESTED_B[2][1].NESTED_C.NESTED_D[3] == "old_conf"
        )
        assert settings.NESTED_A.NESTED_1.NESTED_2 == "new_conf"
        # remove environment variables after testing
        del os.environ["DYNACONF_NESTED_A__nested_1__nested_2"]
        del os.environ[
            "DYNACONF_NESTED_A__nested_b___2___1__nested_c__nested_d___3"
        ]

    @pytest.mark.skipif(
        sys.platform == "win32", reason="Different behavior in Windows"
    )
    def test_docs_example(self, monkeypatch):
        expected = {
            "default": {
                "WORKERS": [{"Address": "1.1.1.1"}, {"Address": "2.2.2.2"}],
            }
        }
        with monkeypatch.context() as m:
            m.setenv(
                "DYNACONF_DATABASES__default__WORKERS___0__Address", "1.1.1.1"
            )
            m.setenv(
                "DYNACONF_DATABASES__default__WORKERS___1__Address", "2.2.2.2"
            )
            settings = Dynaconf()
            assert settings.databases == expected
